winbrew_app\operations\update/
api.rs

1use anyhow::{Context, Result};
2use url::Url;
3
4use crate::core::network::Client;
5use crate::models::catalog::CatalogMetadata;
6
7use super::types::CatalogUpdateResponse;
8
9/// Fetches the catalog update selection.
10///
11/// When local metadata is available, the current catalog hash is sent to the
12/// update API so it can choose between a current, patch, or full-snapshot
13/// response.
14///
15/// # Errors
16/// Returns an error when the request URL is invalid, the request fails, the
17/// API returns a non-success status, or the response body cannot be decoded.
18pub(super) fn fetch_catalog_update_selection(
19    client: &Client,
20    update_api_url: &str,
21    local_metadata: Option<&CatalogMetadata>,
22) -> Result<CatalogUpdateResponse> {
23    fetch_catalog_update_selection_with_current_hash(
24        client,
25        update_api_url,
26        local_metadata.map(|metadata| metadata.current_hash.as_str()),
27    )
28}
29
30/// Fetches a full snapshot update selection.
31///
32/// This bypasses local metadata and always asks the API for a full snapshot
33/// plan.
34///
35/// # Errors
36/// Returns an error when the request URL is invalid, the request fails, the
37/// API returns a non-success status, or the response body cannot be decoded.
38pub(super) fn fetch_full_snapshot_update_selection(
39    client: &Client,
40    update_api_url: &str,
41) -> Result<CatalogUpdateResponse> {
42    fetch_catalog_update_selection_with_current_hash(client, update_api_url, None)
43}
44
45fn fetch_catalog_update_selection_with_current_hash(
46    client: &Client,
47    update_api_url: &str,
48    current_hash: Option<&str>,
49) -> Result<CatalogUpdateResponse> {
50    let api_url = build_update_api_url(update_api_url, current_hash).with_context(|| {
51        format!("failed to build catalog update selection URL from {update_api_url}")
52    })?;
53
54    let response = client
55        .get(api_url.as_str())
56        .send()
57        .with_context(|| format!("failed to send catalog update selection request to {api_url}"))?
58        .error_for_status()
59        .with_context(|| format!("catalog update selection request failed for {api_url}"))?;
60
61    response.json().with_context(|| {
62        format!("failed to decode catalog update selection response from {api_url}")
63    })
64}
65
66fn build_update_api_url(update_api_url: &str, current_hash: Option<&str>) -> Result<Url> {
67    let mut url = Url::parse(update_api_url).context("invalid catalog update selection API URL")?;
68
69    if let Some(current_hash) = current_hash {
70        url.query_pairs_mut().append_pair("current", current_hash);
71    }
72
73    Ok(url)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::build_update_api_url;
79
80    #[test]
81    fn build_update_api_url_appends_current_hash() {
82        let url = build_update_api_url("https://example.invalid/v1/update", Some("sha256:abc123"))
83            .expect("build url");
84
85        assert_eq!(
86            url.as_str(),
87            "https://example.invalid/v1/update?current=sha256%3Aabc123"
88        );
89    }
90
91    #[test]
92    fn build_update_api_url_preserves_existing_query_parameters() {
93        let url = build_update_api_url(
94            "https://example.invalid/v1/update?feature=preview",
95            Some("sha256:abc123"),
96        )
97        .expect("build url");
98
99        assert_eq!(
100            url.as_str(),
101            "https://example.invalid/v1/update?feature=preview&current=sha256%3Aabc123"
102        );
103    }
104}